package org.hepeng.workx.mybatis.spring;

import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.type.TypeHandler;
import org.hepeng.workx.extension.XLoader;
import org.hepeng.workx.jdbc.DataSourceMetadataStore;
import org.hepeng.workx.jdbc.SelectableDataSource;
import org.hepeng.workx.mybatis.MybatisConstant;
import org.hepeng.workx.mybatis.builder.xml.XMLConfigBuilderX;
import org.hepeng.workx.jdbc.DataSourceMetadata;
import org.hepeng.workx.mybatis.datasource.DataSourceRegistry;
import org.hepeng.workx.mybatis.event.listener.ExecuteEventListener;
import org.hepeng.workx.mybatis.session.SqlSessionFactoryBuilderX;
import org.hepeng.workx.mybatis.util.WorkXMybatisEnvironment;
import org.hibernate.dialect.Database;
import org.joor.Reflect;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

import static org.springframework.util.ObjectUtils.isEmpty;
import static org.springframework.util.StringUtils.hasLength;
import static org.springframework.util.StringUtils.tokenizeToStringArray;

/**
 * @author he peng
 */

@Getter
@Setter
public class WorkXSqlSessionFactoryBean extends SqlSessionFactoryBean implements ApplicationContextAware {

    private static final Log LOGGER = LogFactory.getLog(WorkXSqlSessionFactoryBean.class);

    protected Set<ExecuteEventListener> executeEventListeners;
    protected List<DataSourceMetadata> dataSources;
    protected String eventConsumerPackages;
    protected boolean enableDataSourceRoute = false;
    protected boolean enablePublishEvent = false;
    protected Database dialect;
    protected ApplicationContext applicationContext;

    public WorkXSqlSessionFactoryBean() {
        Reflect thisReflect = Reflect.on(this);
        thisReflect.set("sqlSessionFactoryBuilder" , new SqlSessionFactoryBuilderX());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
        Configuration configuration;
        Reflect thisReflect = Reflect.on(this);
        XMLConfigBuilder xmlConfigBuilder = null;
        Configuration thisConfiguration = thisReflect.get("configuration");
        if (thisConfiguration != null) {
            configuration = thisConfiguration;
            if (configuration.getVariables() == null) {
                configuration.setVariables(thisReflect.get("configurationProperties"));
            } else if (thisReflect.get("configurationProperties") != null) {
                configuration.getVariables().putAll(thisReflect.get("configurationProperties"));
            }
        } else if (thisReflect.get("configLocation") != null) {
            Resource thisConfigLocation = thisReflect.get("configLocation");
            xmlConfigBuilder = new XMLConfigBuilderX(thisConfigLocation.getInputStream(), null, thisReflect.get("configurationProperties"));
            configuration = xmlConfigBuilder.getConfiguration();
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
            }

            configuration = XLoader.getXLoader(Configuration.class , MybatisConstant.MYBATIS_X_POINT_DIRECTORY).getX();
            if (thisReflect.get("configurationProperties") != null) {
                configuration.setVariables(thisReflect.get("configurationProperties"));
            }
        }

        variables(configuration);
//        if (thisReflect.get("objectFactory") != null) {
//            configuration.setObjectFactory(thisReflect.get("objectFactory"));
//        }
        SpringObjectFactory springObjectFactory = new SpringObjectFactory(this.applicationContext);
        configuration.setObjectFactory(springObjectFactory);

        if (thisReflect.get("objectWrapperFactory") != null) {
            configuration.setObjectWrapperFactory(thisReflect.get("objectWrapperFactory"));
        }

        if (thisReflect.get("vfs") != null) {
            configuration.setVfsImpl(thisReflect.get("vfs"));
        }

        if (hasLength(thisReflect.get("typeAliasesPackage"))) {
            String[] typeAliasPackageArray = tokenizeToStringArray(thisReflect.get("typeAliasesPackage") ,
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            for (String packageToScan : typeAliasPackageArray) {
                configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                        thisReflect.get("typeAliasesSuperType") == null ? Object.class : thisReflect.get("typeAliasesSuperType"));
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
                }
            }
        }

        if (!isEmpty(thisReflect.get("typeAliases"))) {
            Class<?>[] thisTypeAliases = thisReflect.get("typeAliases");
            for (Class<?> typeAlias : thisTypeAliases) {
                configuration.getTypeAliasRegistry().registerAlias(typeAlias);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered type alias: '" + typeAlias + "'");
                }
            }
        }

        Interceptor[] thisPlugins = thisReflect.get("plugins");
        if (!isEmpty(thisPlugins)) {
            for (Interceptor plugin : thisPlugins) {
                configuration.addInterceptor(plugin);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered plugin: '" + plugin + "'");
                }
            }
        }

        if (hasLength(thisReflect.get("typeHandlersPackage"))) {
            String[] typeHandlersPackageArray = tokenizeToStringArray(thisReflect.get("typeHandlersPackage") ,
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            for (String packageToScan : typeHandlersPackageArray) {
                configuration.getTypeHandlerRegistry().register(packageToScan);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
                }
            }
        }

        TypeHandler<?>[] thisTypeHandlers = thisReflect.get("typeHandlers");
        if (!isEmpty(thisTypeHandlers)) {
            for (TypeHandler<?> typeHandler : thisTypeHandlers) {
                configuration.getTypeHandlerRegistry().register(typeHandler);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered type handler: '" + typeHandler + "'");
                }
            }
        }

        DatabaseIdProvider thisDatabaseIdProvider = thisReflect.get("databaseIdProvider");
        if (thisDatabaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
            try {
                configuration.setDatabaseId(thisDatabaseIdProvider.getDatabaseId(thisReflect.get("dataSource")));
            } catch (SQLException e) {
                throw new NestedIOException("Failed getting a databaseId", e);
            }
        }

        Cache thisCache = thisReflect.get("cache");
        if (thisCache != null) {
            configuration.addCache(thisCache);
        }

        if (xmlConfigBuilder != null) {
            try {
                Object sqlSessionFactoryBuilder = thisReflect.get("sqlSessionFactoryBuilder");
                Reflect.on(sqlSessionFactoryBuilder).set("xmlConfigBuilder" , xmlConfigBuilder);
                xmlConfigBuilder.parse();

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Parsed configuration file: '" + thisReflect.get("configLocation") + "'");
                }
            } catch (Exception ex) {
                throw new NestedIOException("Failed to parse config resource: " + thisReflect.get("configLocation"), ex);
            } finally {
                ErrorContext.instance().reset();
            }
        }

        if (thisReflect.get("transactionFactory") == null) {
            thisReflect.set("transactionFactory" , new SpringManagedTransactionFactory());
        }

        TransactionFactory thisTransactionFactory = thisReflect.get("transactionFactory");
        DataSource thisDataSource = thisReflect.get("dataSource");

        if (! SelectableDataSource.class.isAssignableFrom(thisDataSource.getClass()) &&
                WorkXMybatisEnvironment.isEnableDataSourceRoute(thisReflect.get("configurationProperties"))) {
            SelectableDataSource selectableDataSource = new SelectableDataSource();
            DataSourceMetadataStore.storeDataSourceMetaData(this.dataSources);
            thisDataSource = selectableDataSource;
        }
        configuration.setEnvironment(new Environment(thisReflect.get("environment"), thisTransactionFactory , thisDataSource));

        Resource[] thisMapperLocations = thisReflect.get("mapperLocations");
        if (!isEmpty(thisMapperLocations)) {
            for (Resource mapperLocation : thisMapperLocations) {
                if (mapperLocation == null) {
                    continue;
                }

                try {
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                            configuration, mapperLocation.toString(), configuration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } catch (Exception e) {
                    throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
                } finally {
                    ErrorContext.instance().reset();
                }

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
                }
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
            }
        }

        SqlSessionFactoryBuilder thisSqlSessionFactoryBuilder = thisReflect.get("sqlSessionFactoryBuilder");
        if (CollectionUtils.isNotEmpty(this.executeEventListeners)) {
            Reflect.on(thisSqlSessionFactoryBuilder).call("executeEventListeners" , this.executeEventListeners);
        }

        Properties configurationProperties = thisReflect.get("configurationProperties");
        if (Objects.isNull(configurationProperties)) {
            thisReflect.set("configurationProperties" , new Properties());
        }

//        dataSourceRegistered(configuration);
        return thisSqlSessionFactoryBuilder.build(configuration);
    }

    protected void variables(Configuration configuration) {
        Properties variables = Reflect.on(this).get("configurationProperties");
        if (Objects.isNull(variables)) {
            variables = new Properties();
        }

        variables.setProperty(WorkXMybatisEnvironment.ENABLE_DATASOURCE_ROUTE , Boolean.toString(this.enableDataSourceRoute));
        variables.setProperty(WorkXMybatisEnvironment.ENABLE_PUBLISH_EVENT , Boolean.toString(this.enablePublishEvent));
        variables.setProperty(WorkXMybatisEnvironment.DIALECT , this.dialect.name());
        if (StringUtils.isNotBlank(this.eventConsumerPackages)) {
            variables.setProperty(MybatisConstant.EVENT_CONSUMER_PACKAGES , this.eventConsumerPackages);
        }
        configuration.setVariables(variables);
        Reflect.on(this).set("configurationProperties" , variables);
    }

    @Deprecated
    protected void dataSourceRegistered(Configuration configuration) {
        DataSourceRegistry dataSourceRegistry = Reflect.on(configuration).get("dataSourceRegistry");
        Map<String , DataSourceMetadata> dataSourceInfoMap = new HashMap<>();
        if (CollectionUtils.isNotEmpty(this.dataSources)) {
            for (DataSourceMetadata dataSourceMetaData : this.dataSources) {
                dataSourceInfoMap.put(dataSourceMetaData.getId() , dataSourceMetaData);
            }
        }
        dataSourceRegistry.addDataSource(dataSourceInfoMap);
    }

}
